package edu.siu.math.egut.util;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.WeakHashMap;

/**
 * The Class Monomial is a representation of the mathematical object of the same
 * name. This class supports multiple variables (as Strings) with integer
 * coefficients . Instantiations of this class are immutable, and as such there
 * is never more than one copy of each distinct monomial. This sharing of
 * objects is aimed at reducing memory usage in large scale applications. This
 * class is heavily based on a similar program written in C++ by Joseph Hundley.
 * 
 * This class also implements comparable which is used by the polynomial class
 * for sorting the composing monomials.
 * 
 * A variable may be any string, however using strange strings will make output
 * less clear to read.  Use of the string "_RESERVED" may cause errors in the 
 * method "commutator" from class "Group."
 * 
 * A monomial may be output using the function "toString()." Please note that
 * toString has been written to minimize space used, without safeguards to
 * distinguish distinct variables from single variables represented by
 * multi-character strings. Thus a monomial consisting of the product of the
 * variables "a" and "b" will look identical to one consisting of the single
 * variable "ab."
 * 
 * @author Joe Pleso
 */
final public class Term implements Comparable<Term>, Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 220L;
    /**
     * This constant is used internally by the egut package. It should
     * not be used by classes that use monomial. Doing so will result in
     * strange results.
     */
    public static final String RESERVED_VARIABLE = "_RESERVED";
    final static private Map<Term, Object> monomials = 
	new WeakHashMap<Term, Object>();
    /**
     * Returns the monomial with the specified variables and exponents. There is
     * at most one monomial object, which is shared, with any specific
     * combination of variables and exponents. This method checks if the
     * requested monomial exists. If not it creates it and adds it to the store
     * of active monomials. Either way the reference to the specified object in
     * the store (either existing from the store or the one just created
     * respectively) is returned. When the monomial is no longer in use (as
     * determined by the JVM), it is removed from the store. This process is
     * completely transparent.
     * 
     * 
     * 
     * 
     * 
     * @param pairs Map which contains pairs of variables and corresponding
     *                integer exponents
     * 
     * @return the Monomial object that has the same set of variables
     */
    public static Term create(Map<String, Integer> pairs) {

	for (Term m : monomials.keySet()) {
	    if (pairs.equals(m.v))
		return m;
	}
	return new Term(pairs);
    }
    
    /**
     * Creates a monomial consisting of a single variable raised to 
     * the first power.
     * 
     * @param s string name for the variable 
     * @return the monomial s^1.
     */
    public static Term create (String s){
	  Map<String, Integer> preMonomial = new HashMap<String, Integer>();
	  preMonomial.put(s,1);
	  return create(preMonomial);
  }
    
    /**
     * Creates a monomial consisting of a specified variable raised to
     *  a specified power.
     * 
     * @param s String, interpreted as variable
     * @param e Integer, interpreted as exponent
     * @return  the Monomial object representing the monomial s^e.
     */
    public static Term create (String s, int e){
	  Map<String, Integer> preMonomial = new HashMap<String, Integer>();
	  preMonomial.put(s,e);
	  return create(preMonomial);
  }
   
    /**
     * Reads a string containing variables, *, and ^ into a
     * monomial.
     * 
     * @param s the string to be read
     * @return the monomial
     * @throws TermReadException thrown if there was an error reading the
     * string
     */
    public static Term read (String s) throws TermReadException{
	  Map<String, Integer> preMonomial = new HashMap<String, Integer>();
	     
	  
	  // break into factors according to *
	  String[] factors = s.split("\\*");
	  int numberOfFactors = factors.length;

	  // for each factor, parse the string into a Monomial
	  for( int j = 0 ; j < numberOfFactors; j++)
	  {
	      while (!(factors[j].length()==0)){
	      factors[j] = factors[j].trim();
	      // split into a base and an exponent if there is a ^
	      String[] baseExpPair = factors[j].split("\\^",2);
	      // if there are two ^'s we don't know how to handle it.
	      
	      baseExpPair[0] = baseExpPair[0].trim();
	      int exponent;
	      if (baseExpPair.length == 1){
		  exponent =1 ;
		  factors[j] = "";
	      }else{
		 int i = 0;
		  
		 for (baseExpPair[1] = baseExpPair[1].trim();
		 i<baseExpPair[1].length();
		 i++){
		     if (!Character.isDigit(baseExpPair[1].charAt(i)))
			 break;
		 }
		if (i == 0)
		    exponent = 1;
		else
		 exponent = Integer.parseInt(baseExpPair[1].substring(0, i)
			      );
		factors[j] = baseExpPair[1].substring(i);
	      }
	      
	      if (preMonomial.put(
		      baseExpPair[0], exponent) != null)
		  throw new TermReadException("Too many " + baseExpPair[0]);
	     
	      }

	  }
	  return create(preMonomial);

	     
	     
    }

    final private TreeMap<String, Integer> v;

    private Term(Map<String, Integer> d) {

	v = new TreeMap<String, Integer>(d);

	monomials.put(this, null);
    }

    public int compareTo(Term o) {
	if (o == null)
	    throw new NullPointerException();
	if (o == this)
	    return 0;
	/*
	 * mononomials of higher total degree come first. variables appear in
	 * alphabetical order. monomials of same total degree are sorted first
	 * by exponent of the "first" variable, then by the "second," etc.
	 */

	if (this.getDegree() != o.getDegree())
	    return this.getDegree() - o.getDegree();

	Iterator<Map.Entry<String, Integer>> oI = o.v.entrySet().iterator();

	for (Map.Entry<String, Integer> tE : v.entrySet()) {
	    Map.Entry<String, Integer> oE = oI.next();
	    if (!tE.getKey().equals(oE.getKey()))
		return tE.getKey().compareTo(oE.getKey());
	    if (tE.getValue() != oE.getValue())
		return -tE.getValue() + oE.getValue();
	}
	
	throw new RuntimeException();

    }

    /**
     * Factor out one power of a single variable. This can only be called if the
     * monomial is not independent of the variable.
     * 
     * @param x  the name of the variable to be factored out
     * 
     * @return the monomial representation of the new monomial
     * @throws TermVariableMissingException   if the monomial is independent
     *  of the specified variable
     */
    
    public Term factorOut(String x) throws TermVariableMissingException {
	if (isIndependentOf(x))
	    throw new TermVariableMissingException(x);
	SortedMap<String, Integer> newV = new TreeMap<String, Integer>(v);
	Integer newExponent = v.get(x) - 1;

	if (newExponent == 0)
	    newV.remove(x);
	else
	    newV.put(x, newExponent);

	return create(newV);

    }

    /**
     * This method returns the sum of all the exponents in this monomial.
     * 
     * @return the degree of the monomial
     */
    public int getDegree() {
	int sum = 0;
	for (Integer i : this.v.values())
	    sum += i* i>0? 1:-1;
	return sum;
    }
    
    /**
     * This method determines the exponent of a specified variable in the
     *  monomial which calls it.
     * @param s String, interpreted as the name of a variable
     * @return the exponent of s in the monomial, or 0 if s does not appear.
     */
    public int degreeIn( String s ){
	
	 return  v.containsKey(s)? v.get(s): 0;
	
	
    }
    
    /**
     * Get a variable that appears nontrivially.
     * @return a string which is one of the variables in the monomial.
     * 
     */
    public String getVar(){
	if (v.isEmpty())
	    return null;
	return this.v.lastKey();
	
    }

    /**
     * Gets the exponent of a specified variable.
     * 
     * @param name              the name of the variable to check the exponent
     * 
     * @return the exponent of the variable we are checking
     */
    public int getExponent(String name) {
	return v.get(name) == null ? 0 : v.get(name);

    }

    // Are these the right defs of linear?

    /**
     * Returns the number of distinct variables that comprise this monomial.
     * (The number of variable, exponent pairs stored in this monomial)
     * 
     * @return the number of distinct variables that comprise this monomial
     * 
     * 
     * 
     */
    public int getSize() {
	return v.size();
    }

    /**
     * Checks if the Monomial is independent of a variable.
     * 
     * @param name            interpreted as the name of a variable
     * 
     * @return false if the specified variable appears in the Monomial and true
     *         if it does not.
     */
    public boolean isIndependentOf(String name) {
	return v.get(name) == null;

    }

    // These can possibly be removed at some point
    /**
     * Returns true if the monomial is linear -- i.e., consists of a single
     * variable raised to the first power-- and false otherwise.
     * 
     * @return the value of getDegree()==1
     */
    public boolean isLinear() {
	return getDegree() == 1;

    }

    // These can possibly be removed at some point
    /**
     * Returns true if the given variable appears in the monomial with an
     * exponent of 1, and false otherwise.
     * 
     * @param name           the name of the variable we are checking
     * 
     * @return true, if the exponent of the named variable is 1
     */
    public boolean isLinearIn(String name) {
	return v.get(name) != null && v.get(name) == 1;

    }

    /**
     * Multiplies this monomial with another and returns the resulting monomial.
     * 
     * 
     * 
     * @param b           the monomial by which to multiply
     * 
     * @return the monomial that represents the product
     */
    public Term multiply(Term b) {

	Term a = this;

	if (a.v.isEmpty())
	    return b;
	if (b.v.isEmpty())
	    return a;
	Map<String, Integer> newV = new TreeMap<String, Integer>(a.v);
	for (Map.Entry<String, Integer> entry : b.v.entrySet()) {
	    if (!newV.containsKey(entry.getKey()))
		newV.put(entry.getKey(), entry.getValue());
	    else {
		Integer newExponent = entry.getValue()
			+ newV.get(entry.getKey());
		if( newExponent != 0 )
		    newV.put(entry.getKey(), newExponent);
		else 
		    newV.remove(entry.getKey());
	    }
	}

	return create(newV);
    }

    /**
     *Convenience method.
     * 
     * @param s string to become term which will be multiplied to this
     * @return the result
     */
    public Term times(String s){
	return this.multiply(Term.create(s));
    }
    
    /**
     * Completely factors out a variable.
     * 
     * @param varName        the name of the variable to be factored out
     * @return a monomial that is independent of the named variable. If the
     *         original monomial had this property, then the original monomial
     *         will be returned.
     */
    public Term remove(final String varName) {
	SortedMap<String, Integer> result = new TreeMap<String, Integer>(v);
	result.remove(varName);
	return create(result);
    }

    /**
     * Returns the string representation of the monomial.
     * 
     * @return the string
     * 
     * @see java.lang.Object#toString()
     */

    public String toString() {

	final StringBuffer sb = new StringBuffer();
	for (final Map.Entry<String, Integer> x : v.entrySet()) {
	    sb.append(x.getKey());
	    if (x.getValue() != 1)
		sb.append("^{" + x.getValue()+"}");
	    sb.append(" ");

	}
	return sb.toString();
    }

    /**
     * Returns the inverse of this term.
     * 
     * @return the inverse of this term
     */
    public Term inverse() {
	Map<String, Integer> clone = new TreeMap<String,Integer>();
	for (Map.Entry<String, Integer> tE : v.entrySet()){
	    clone.put(tE.getKey(),-tE.getValue());
	}
	return create(clone);
    }

    /**
     *Returns the set of variables appearing in this term.
     * 
     * @return the set of variables appearing in this term
     */
    public Collection<? extends String> getVariables() {
	return v.keySet();
    }

}
